{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# RunnableWithMessageHistory\n",
    "\n",
    "- Author: [Secludor](https://github.com/Secludor)\n",
    "- Peer Review: \n",
    "- Proofread : [Chaeyoon Kim](https://github.com/chaeyoonyunakim)\n",
    "- This is a part of [LangChain Open Tutorial](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/13-LangChain-Expression-Language/08-RunnableWithMessageHistory.ipynb)[![Open in GitHub](https://img.shields.io/badge/Open%20in%20GitHub-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/13-LangChain-Expression-Language/08-RunnableWithMessageHistory.ipynb)\n",
    "## Overview\n",
    "\n",
    "```RunnableWithMessageHistory``` in LangChain's Expression Language (LCEL) for **managing conversation history** in chatbots, virtual assistants, and other conversational AI applications. It seamlessly integrates with existing LangChain components **to automatically handle message history management and updates.**\n",
    "\n",
    "### Key Features\n",
    "\n",
    "**Message History Management**\n",
    "- Maintains conversation context across multiple interactions.\n",
    "- Automatically tracks and stores chat messages.\n",
    "- Enables contextual responses based on previous conversations.\n",
    "\n",
    "**Flexible Input/Output Support**\n",
    "- Handles both message objects and Python dictionaries.\n",
    "- Supports various input formats, including:\n",
    "  - Single messages\n",
    "  - Message sequences\n",
    "  - Dictionary inputs with custom keys\n",
    "- Provides consistent output handling regardless of input format.\n",
    "\n",
    "**Session Management**\n",
    "- Manages conversations through unique identifiers, such as:\n",
    "  - Simple session IDs\n",
    "  - Combined user and conversation IDs\n",
    "- Maintains separate conversation threads for different users or contexts.\n",
    "  - Ensures conversation continuity within the same session.\n",
    "\n",
    "**Storage Options**\n",
    "- Offers in-memory storage for development and testing.\n",
    "- Supports persistent storage (e.g., Redis, files) for production environments.\n",
    "- Provides easy integration with various storage backends.\n",
    "\n",
    "**Advantages Over Legacy Approaches**\n",
    "- More flexible than the older ConversationChain.\n",
    "- Offers better state management.\n",
    "- Provides improved integration with modern LangChain components.\n",
    "\n",
    "### Summary\n",
    "```RunnableWithMessageHistory``` is the recommended standard for conversation management in LangChain, offering:\n",
    "- Simplified conversation state management.\n",
    "- An enhanced user experience through context preservation.\n",
    "- Flexible configuration options for diverse use cases.\n",
    "\n",
    "### Table of Contents\n",
    "\n",
    "- [Overview](#overview)  \n",
    "- [Environment Setup](#environment-setup)  \n",
    "- [Getting Started with RunnableWithMessageHistory](#getting-started-with-runnablewithmessagehistory)\n",
    "- [Understanding In-Memory Conversation History](#understanding-in-memory-conversation-history)\n",
    "- [Example of Runnables Using Different Keys](#example-of-runnables-using-different-keys)\n",
    "- [Persistent Storage](#persistent-storage)\n",
    "- [Using Redis for Persistence](#using-redis-for-persistence)\n",
    "\n",
    "### References\n",
    "\n",
    "- [LangChain Core API Documentation - RunnableWithMessageHistory](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.history.RunnableWithMessageHistory.html#langchain_core.runnables.history.RunnableWithMessageHistory)\n",
    "- [LangChain Documentation - Message History](https://python.langchain.com/docs/how_to/message_history/)\n",
    "- [LangChain's message histories: memory integrations](https://python.langchain.com/docs/integrations/memory/)\n",
    "- [LangServe's example of a chat server with persistent storage](https://github.com/langchain-ai/langserve/blob/main/examples/chat_with_persistence_and_user/server.py)\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Environment Setup\n",
    "\n",
    "Setting up your environment is the first step. See the [Environment Setup](https://wikidocs.net/257836) guide for more details.\n",
    "\n",
    "**[Note]**\n",
    "- The ```langchain-opentutorial``` is a bundle of easy-to-use environment setup guidance, useful functions and utilities for tutorials. \n",
    "- Check out the [```langchain-opentutorial```](https://github.com/LangChain-OpenTutorial/langchain-opentutorial-pypi) for more details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install langchain-opentutorial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install required packages\n",
    "from langchain_opentutorial import package\n",
    "\n",
    "package.install(\n",
    "    [\n",
    "        \"langsmith\",\n",
    "        \"langchain_openai\",\n",
    "        \"langchain_core\",\n",
    "        \"langchain_community\",\n",
    "    ],\n",
    "    verbose=False,\n",
    "    upgrade=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Environment variables have been set successfully.\n"
     ]
    }
   ],
   "source": [
    "# Set environment variables\n",
    "from langchain_opentutorial import set_env\n",
    "\n",
    "set_env(\n",
    "    {\n",
    "        \"OPENAI_API_KEY\": \"\",\n",
    "        \"LANGCHAIN_API_KEY\": \"\",\n",
    "        \"LANGCHAIN_TRACING_V2\": \"true\",\n",
    "        \"LANGCHAIN_ENDPOINT\": \"https://api.smith.langchain.com\",\n",
    "        \"LANGCHAIN_PROJECT\": \"08-RunnableWithMessageHistory\",\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively, you can set and load ```OPENAI_API_KEY``` from a ```.env``` file.\n",
    "\n",
    "**[Note]** This is only necessary if you haven't already set ```OPENAI_API_KEY``` in previous steps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Getting Started with ```RunnableWithMessageHistory```\n",
    "\n",
    "Managing conversation history is crucial for conversational applications and complex data processing tasks. ```RunnableWithMessageHistory``` simplifies the message history implementation. To use it effectively, you need these two key components:\n",
    "\n",
    "1. **Runnable objects**,\n",
    "  - Creating Runnable objects, such as ```retriever``` or ```chain```, are the primary components that interacts with ```BaseChatMessageHistory```. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are an assistant skilled in {ability}. Keep responses under 20 words.\",\n",
    "        ),\n",
    "        # Use the conversation history as a variable, with 'history' as the key for MessageHistory.\n",
    "        MessagesPlaceholder(variable_name=\"history\"),\n",
    "        (\"human\", \"{input}\"),  # Use user input as a variable\n",
    "    ]\n",
    ")\n",
    "runnable = (\n",
    "    prompt | model\n",
    ")  # Create a runnable object by connecting the prompt and the model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. **Message History Manager (callable)**\n",
    "\n",
    "- This is a callable that returns an instance of ```BaseChatMessageHistory```. It handles message storage, retrieval, updates, and maintains conversation context for contextual responses.\n",
    "\n",
    "### Implementation Options\n",
    "\n",
    "LangChain offers several implementations for managing message history. You can explore various memory integrations for persistent storage, as documented in the [LangChain's message histories: memory integrations](https://python.langchain.com/docs/integrations/memory/) page.\n",
    "\n",
    "This tutorial covers two primary approaches in implementation:\n",
    "\n",
    "1. **In-Memory ```ChatMessageHistory```**\n",
    "   - Manages message history in memory, making it ideal for development and simple applications.\n",
    "   - Provides fast access speeds.\n",
    "   - Message history is lost on application restart.\n",
    "\n",
    "2. **Persistent Storage with ```RedisChatMessageHistory```**\n",
    "   - Enables permanent message storage using Remote Dictionary Server (Redis), a high-performance, open-source in-memory data structure store.\n",
    "   - Suitable for distributed environments.\n",
    "   - Ideal for complex applications and long-running services.\n",
    "\n",
    "- Consider these factors when selecting a message history management approach:\n",
    "   - Application requirements\n",
    "   - Expected traffic volume\n",
    "   - Message data importance\n",
    "   - Retention period requirements\n",
    "\n",
    "While in-memory implementation offers simplicity and speed, persistent storage solutions like Redis are more appropriate when data durability is a concern."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Understanding In-Memory Conversation History\n",
    "\n",
    "In-memory conversation history provides a simple and fast way to manage chat message history during development and testing. This approach stores conversation data in memory, offering quick access but without persistence across application restarts.\n",
    "\n",
    "### Core Configuration Parameters\n",
    "\n",
    "**Required Components**\n",
    "- ```runnable```: The chain or model (e.g., ChatOpenAI) to execute.\n",
    "- ```get_session_history```: A function returning a ```BaseChatMessageHistory``` instance.\n",
    "- ```input_messages_key```: Specifies the key for user input in ```invoke()``` calls.\n",
    "- ```history_messages_key```: Defines the key for accessing conversation history."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.chat_message_histories import ChatMessageHistory\n",
    "from langchain_core.chat_history import BaseChatMessageHistory\n",
    "from langchain_core.runnables.history import RunnableWithMessageHistory\n",
    "\n",
    "store = {}  # Initialize empty store for message histories.\n",
    "\n",
    "\n",
    "# function for getting session logs by session_id.\n",
    "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n",
    "    print(session_id)\n",
    "    if session_id not in store:  # if session_id isn't in the store,\n",
    "        # create new ChatMessageHistory and put it in the store.\n",
    "        store[session_id] = ChatMessageHistory()\n",
    "    return store[session_id]  # return the session logs for session_id.\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"input\",\n",
    "    history_messages_key=\"history\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Default Session Implementation\n",
    "```RunnableWithMessageHistory``` uses ```session_id``` as its default identifier for managing conversation threads, as shown in its core implementation:\n",
    "\n",
    "```python\n",
    "if history_factory_config:\n",
    "    _config_specs = history_factory_config\n",
    "else:\n",
    "    # If not provided, then we'll use the default session_id field\n",
    "    _config_specs = [\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"session_id\",\n",
    "            annotation=str,\n",
    "            name=\"Session ID\",\n",
    "            description=\"Unique identifier for a session.\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "    ]\n",
    "```\n",
    "### Using Session Management\n",
    "To utilize session management, specify a session ID in your invoke call:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "abc123\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='Cosine is a trigonometric function representing the adjacent side over hypotenuse in a right triangle.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 31, 'total_tokens': 53, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_5f20662549', 'finish_reason': 'stop', 'logprobs': None}, id='run-4afc17da-4dd9-46ff-888c-fff45266820e-0', usage_metadata={'input_tokens': 31, 'output_tokens': 22, 'total_tokens': 53, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using the same ```session_id``` continues the conversation by retrieving the previous thread's content (this continuous conversation is called a **session**):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "abc123\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='코사인은 직각 삼각형에서 인접 변을 빗변으로 나눈 값을 나타내는 삼각 함수입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 67, 'total_tokens': 96, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_5f20662549', 'finish_reason': 'stop', 'logprobs': None}, id='run-a3207321-ffe4-4c85-b89c-d418967f1ee2-0', usage_metadata={'input_tokens': 67, 'output_tokens': 29, 'total_tokens': 96, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Call with message history\n",
    "with_message_history.invoke(\n",
    "    # Set ability and input\n",
    "    {\"ability\": \"math\", \"input\": \"Please translate the previous content to Korean\"},\n",
    "    # Specify configuration options\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, using a different ```session_id``` will result in an inaccurate response because there is no corresponding history. \n",
    "\n",
    "For example, if ```session_id``` is ```def234``` and no history exists for that ID, you'll see an irrelevant response (see the following code snippet)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "def234\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='이전 내용을 한국어로 번역해드릴 수 없습니다. 수학 관련 질문이 있으면 도와드리겠습니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 28, 'prompt_tokens': 33, 'total_tokens': 61, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_703d4ff298', 'finish_reason': 'stop', 'logprobs': None}, id='run-277d6cc5-ca04-40a5-a2a3-ff67e31e72ec-0', usage_metadata={'input_tokens': 33, 'output_tokens': 28, 'total_tokens': 61, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# New session_id means no previous conversation memory\n",
    "with_message_history.invoke(\n",
    "    # Pass math ability and input message\n",
    "    {\"ability\": \"math\", \"input\": \"Please translate the previous content to Korean\"},\n",
    "    # Set a new session_id\n",
    "    config={\"configurable\": {\"session_id\": \"def234\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can customize the configuration parameters for tracking message history by passing a list of ```ConfigurableFieldSpec``` objects through the ```history_factory_config``` parameter.\n",
    "\n",
    "Setting a new ```history_factory_config``` overrides the existing ```session_id``` configuration.\n",
    "\n",
    "The following example demonstrates using two parameters: ```user_id``` and ```conversation_id```."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import ConfigurableFieldSpec\n",
    "\n",
    "store = {}  # Initialize empty store for message histories.\n",
    "\n",
    "\n",
    "def get_session_history(user_id: str, conversation_id: str) -> BaseChatMessageHistory:\n",
    "    # Return session history for given user_id and conversation_id combination.\n",
    "    if (user_id, conversation_id) not in store:\n",
    "        store[(user_id, conversation_id)] = ChatMessageHistory()\n",
    "    return store[(user_id, conversation_id)]\n",
    "\n",
    "\n",
    "# Configure RunnableWithMessageHistory with custom identifiers\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"input\",\n",
    "    history_messages_key=\"history\",\n",
    "    history_factory_config=[  # replacing the \"session_id\"\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"user_id\",\n",
    "            annotation=str,\n",
    "            name=\"User ID\",\n",
    "            description=\"Unique identifier for user.\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"conversation_id\",\n",
    "            annotation=str,\n",
    "            name=\"Conversation ID\",\n",
    "            description=\"Unique identifier for conversation.\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "    ],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's try a custom configuration."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='Cosine is a trigonometric function representing the adjacent side over the hypotenuse in a right triangle.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 30, 'total_tokens': 53, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_703d4ff298', 'finish_reason': 'stop', 'logprobs': None}, id='run-ebd536b5-e599-4c68-b8b8-e011d43b1d0c-0', usage_metadata={'input_tokens': 30, 'output_tokens': 23, 'total_tokens': 53, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"what is cosine?\"},\n",
    "    config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"abc\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example of Runnables Using Different Keys\n",
    "\n",
    "This example demonstrates how to handle inputs and output messages with ```RunnableWithMessageHistory```.\n",
    "\n",
    "### Messages Input with Dictionary Output\n",
    "\n",
    "**Direct Message Object Handling**\n",
    "- Omitting ```input_messages_key=\"input\"``` configures the system to accept ```Message``` objects as input."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content='The cosine of an angle in a right-angled triangle is defined as the ratio of the length of the adjacent side to the length of the hypotenuse. In mathematical terms, for an angle \\\\( \\\\theta \\\\):\\n\\n\\\\[\\n\\\\cos(\\\\theta) = \\\\frac{\\\\text{Adjacent side}}{\\\\text{Hypotenuse}}\\n\\\\]\\n\\nIn the context of the unit circle, which is a circle with a radius of 1 centered at the origin of a coordinate plane, the cosine of an angle \\\\( \\\\theta \\\\) is the x-coordinate of the point where the terminal side of the angle intersects the circle. This definition extends the concept of cosine to all real numbers, not just angles in right triangles.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 144, 'prompt_tokens': 14, 'total_tokens': 158, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_b7d65f1a5b', 'finish_reason': 'stop', 'logprobs': None}, id='run-6e321281-a1c6-4ea9-a4d4-3b7752534c74-0', usage_metadata={'input_tokens': 14, 'output_tokens': 144, 'total_tokens': 158, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})}"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "from langchain_core.runnables import RunnableParallel\n",
    "\n",
    "# Create a chain that outputs dictionary with message.\n",
    "chain = RunnableParallel(\n",
    "    {\"output_message\": ChatOpenAI(model_name=\"gpt-4o\", temperature=0)}\n",
    ")\n",
    "\n",
    "\n",
    "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n",
    "    # Create new ChatMessageHistory if session doesn't exist.\n",
    "    if session_id not in store:\n",
    "        store[session_id] = ChatMessageHistory()\n",
    "    return store[session_id]\n",
    "\n",
    "\n",
    "# Create RunnableWithMessageHistory with message history capability.\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    chain,\n",
    "    get_session_history,\n",
    "    # input_messages_key=\"input\" is omitted to accept Message objects\n",
    "    output_messages_key=\"output_message\",  # Specify output format as dictionary\n",
    ")\n",
    "\n",
    "# Execute chain with message input\n",
    "with_message_history.invoke(\n",
    "    [HumanMessage(content=\"what is the definition of cosine?\")],\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content='코사인의 정의는 직각삼각형에서 한 각의 코사인은 그 각의 인접 변의 길이를 빗변의 길이로 나눈 비율로 정의됩니다. 수학적으로, 각 \\\\( \\\\theta \\\\)에 대해 다음과 같이 표현됩니다:\\n\\n\\\\[\\n\\\\cos(\\\\theta) = \\\\frac{\\\\text{인접 변}}{\\\\text{빗변}}\\n\\\\]\\n\\n단위원에서의 맥락에서는, 단위원은 좌표 평면의 원점에 중심을 두고 반지름이 1인 원입니다. 각 \\\\( \\\\theta \\\\)의 코사인은 각의 종단선이 원과 만나는 점의 x좌표입니다. 이 정의는 코사인의 개념을 직각삼각형의 각뿐만 아니라 모든 실수로 확장합니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 174, 'prompt_tokens': 173, 'total_tokens': 347, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_5f20662549', 'finish_reason': 'stop', 'logprobs': None}, id='run-4eeff02e-73fe-4b80-aeb3-25a9815c8a4d-0', usage_metadata={'input_tokens': 173, 'output_tokens': 174, 'total_tokens': 347, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})}"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Continue conversation with same session ID\n",
    "with_message_history.invoke(\n",
    "    [HumanMessage(content=\"Please translate the previous content to Korean!\")],\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This configuration provides:\n",
    "- Direct handling of the input ```Message``` object.\n",
    "-  Outputting data in a dictionary format.\n",
    "- Maintaining conversation history across sessions.\n",
    "- Continuing conversations seamlessly using session IDs."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ```Message``` Objects for both Input and Output\n",
    "\n",
    "Continuing from the previous example, you can also configure ```RunnableWithMessageHistory``` to handle ```Message``` objects directly for both input and output.\n",
    "\n",
    "**Direct Message Object Handling**\n",
    "- Omitting ```output_messages_key=\"output_message\"``` configures the system to return ```Message``` objects as output."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "with_message_history = RunnableWithMessageHistory(\n",
    "    ChatOpenAI(model_name=\"gpt-4o\", temperature=0),  # Use ChatOpenAI language model\n",
    "    get_session_history,  # Function to retrieve conversation history\n",
    "    # input_messages_key=\"input\",     # Omit to accept Message objects as input\n",
    "    # output_messages_key=\"output_message\"  # Omit to return Message objects as output\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='The term \"cosine\" can refer to a few different concepts depending on the context, but it is most commonly associated with trigonometry in mathematics. Here are the primary meanings:\\n\\n1. **Trigonometric Function**: In trigonometry, the cosine of an angle in a right triangle is defined as the ratio of the length of the adjacent side to the length of the hypotenuse. If \\\\(\\\\theta\\\\) is an angle in a right triangle, then:\\n   \\\\[\\n   \\\\cos(\\\\theta) = \\\\frac{\\\\text{Adjacent side}}{\\\\text{Hypotenuse}}\\n   \\\\]\\n   The cosine function is one of the fundamental trigonometric functions, along with sine and tangent.\\n\\n2. **Unit Circle Definition**: In the context of the unit circle, which is a circle with a radius of 1 centered at the origin of a coordinate plane, the cosine of an angle \\\\(\\\\theta\\\\) is the x-coordinate of the point on the unit circle that is reached by moving counterclockwise from the positive x-axis by an angle \\\\(\\\\theta\\\\).\\n\\n3. **Cosine Function in Calculus**: The cosine function, often written as \\\\(\\\\cos(x)\\\\), is a periodic function with a period of \\\\(2\\\\pi\\\\). It is an even function, meaning \\\\(\\\\cos(-x) = \\\\cos(x)\\\\), and it is used extensively in calculus and analysis.\\n\\n4. **Cosine Similarity**: In the context of data analysis and machine learning, cosine similarity is a measure of similarity between two non-zero vectors. It is calculated as the cosine of the angle between the two vectors, which is the dot product of the vectors divided by the product of their magnitudes. It is often used in text analysis and information retrieval.\\n\\nThese are the primary contexts in which the term \"cosine\" is used, each with its own specific meaning and application.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 389, 'prompt_tokens': 14, 'total_tokens': 403, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_d28bcae782', 'finish_reason': 'stop', 'logprobs': None}, id='run-0ba9374f-a492-4770-b1df-a0bf740b57e0-0', usage_metadata={'input_tokens': 14, 'output_tokens': 389, 'total_tokens': 403, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Invoke with Message object input\n",
    "with_message_history.invoke(\n",
    "    [HumanMessage(content=\"What is the meaning of cosine?\")],\n",
    "    config={\"configurable\": {\"session_id\": \"def123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Dictionary with Single Key for All Messages\n",
    "\n",
    "**Using a Single Key for Input/Output**\n",
    "- This approach uses one key for both input and output messages.\n",
    "- It utilizes ```itemgetter(\"input_messages\")``` to extract input messages from the dictionary.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    itemgetter(\"input_messages\")\n",
    "    | ChatOpenAI(model_name=\"gpt-4o\", temperature=0),  # Extract and process messages.\n",
    "    get_session_history,  # Session history management\n",
    "    input_messages_key=\"input_messages\",  # Specify input message key\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='The term \"cosine\" refers to a trigonometric function that is fundamental in mathematics, particularly in the study of triangles and periodic phenomena. In the context of a right-angled triangle, the cosine of an angle is defined as the ratio of the length of the adjacent side to the length of the hypotenuse. Mathematically, for an angle \\\\( \\\\theta \\\\), it is expressed as:\\n\\n\\\\[\\n\\\\cos(\\\\theta) = \\\\frac{\\\\text{Adjacent side}}{\\\\text{Hypotenuse}}\\n\\\\]\\n\\nIn the unit circle, which is a circle with a radius of one centered at the origin of a coordinate plane, the cosine of an angle \\\\( \\\\theta \\\\) is the x-coordinate of the point where the terminal side of the angle intersects the circle.\\n\\nCosine is also an even function, meaning that \\\\( \\\\cos(-\\\\theta) = \\\\cos(\\\\theta) \\\\), and it has a range of values from -1 to 1. It is periodic with a period of \\\\( 2\\\\pi \\\\), meaning that \\\\( \\\\cos(\\\\theta + 2\\\\pi) = \\\\cos(\\\\theta) \\\\).\\n\\nCosine is widely used in various fields such as physics, engineering, and computer science, particularly in wave analysis, signal processing, and the study of oscillations.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 266, 'prompt_tokens': 14, 'total_tokens': 280, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_d28bcae782', 'finish_reason': 'stop', 'logprobs': None}, id='run-7616dbf4-706f-4f2b-ba66-a32f863db461-0', usage_metadata={'input_tokens': 14, 'output_tokens': 266, 'total_tokens': 280, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Invoke with dictionary input\n",
    "with_message_history.invoke(\n",
    "    {\"input_messages\": \"What is the meaning of cosine?\"},\n",
    "    config={\"configurable\": {\"session_id\": \"xyz123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This configuration enables:\n",
    "- Direct handling of ```Message``` objects.\n",
    "- Simplified input/output processing.\n",
    "- Flexible conversion between different message formats.\n",
    "- Consistent session management."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Understanding Persistent Storage\n",
    "\n",
    "Persistent storage ensures that data is retained **even after a program terminates or the system restarts** . This is typically achieved using databases, file systems, or other non-volatile storage devices.\n",
    "\n",
    "Persistent storage is **essential for long-term data preservation** in applications. It enables.:\n",
    "- State preservation across sessions.\n",
    "- User preference retention.\n",
    "- **Continuous operation without data loss** .\n",
    "- Recovery from previous execution points.\n",
    "\n",
    "\n",
    "### Implementation Options\n",
    "\n",
    "```RunnableWithMessageHistory``` offers flexible storage options that are independent of how ```get_session_history``` retrieves the chat message history.\n",
    "- It supports the local file system (see an example [here](https://github.com/langchain-ai/langserve/blob/main/examples/chat_with_persistence_and_user/server.py))\n",
    "- It integrates with various storage providers (see [LangChain's message histories: memory integrations](https://python.langchain.com/docs/integrations/memory/))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Using Redis for Persistence\n",
    "\n",
    "This section demonstrates how to use Redis for persistent message history storage.\n",
    "\n",
    " 1. **Installation** "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "%pip install -qU redis"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. **Redis Server Setup**\n",
    "\n",
    "Launch a local Redis Stack server using Docker: \n",
    "\n",
    "```bash\n",
    "docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest\n",
    "```\n",
    "Configuration options:\n",
    "- ```-d``` : Run in daemon mode (background).\n",
    "- ```-p {port}:6379``` : Redis server port mapping.\n",
    "- ```-p 8001:8001``` : RedisInsight UI port mapping.\n",
    "- ```redis/redis-stack:latest``` : Latest Redis Stack image.\n",
    "\n",
    "**Tips for Troubleshooting**\n",
    "- Verify Docker is running.\n",
    "- Check port availability (terminate any processes using the port or use different ports)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. **Redis Connection**\n",
    "- Set up the Redis connection URL: ```\"redis://localhost:{port}/0\"```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "REDIS_URL = \"redis://localhost:6379/0\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Implementing Redis Message History\n",
    "\n",
    "To use Redis for message history, define a new callable that returns an instance of ```RedisChatMessageHistory``` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.chat_message_histories.redis import RedisChatMessageHistory\n",
    "\n",
    "\n",
    "def get_message_history(session_id: str) -> RedisChatMessageHistory:\n",
    "    # Return RedisChatMessageHistory instance based on session ID.\n",
    "    return RedisChatMessageHistory(session_id, url=REDIS_URL)\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,  # Runnable object\n",
    "    get_message_history,  # Message history retrieval\n",
    "    input_messages_key=\"input\",  # Key for input messages\n",
    "    history_messages_key=\"history\",  # Key for history messages\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Testing Conversation Continuity\n",
    "\n",
    "**First Interaction**\n",
    "- You can call the function / chain as before."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='Cosine is the ratio of the length of the adjacent side to the hypotenuse in a right triangle.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 214, 'total_tokens': 237, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_5f20662549', 'finish_reason': 'stop', 'logprobs': None}, id='run-08fb9a1f-b16d-4e19-9c2d-404052d3b111-0', usage_metadata={'input_tokens': 214, 'output_tokens': 23, 'total_tokens': 237, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Initial query with new session ID\n",
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n",
    "    config={\"configurable\": {\"session_id\": \"redis123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Continuing the Conversation**\n",
    "- Make the second call using the same ```session_id``` ."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='코사인은 직각삼각형에서 인접 변의 길이를 빗변의 길이로 나눈 비율입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 251, 'total_tokens': 280, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_5f20662549', 'finish_reason': 'stop', 'logprobs': None}, id='run-8256bc74-2094-4091-b34e-075bf5d973ca-0', usage_metadata={'input_tokens': 251, 'output_tokens': 29, 'total_tokens': 280, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Second query using same session ID\n",
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"Please translate the previous response to Korean\"},\n",
    "    config={\"configurable\": {\"session_id\": \"redis123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Testing with Different Session**\n",
    "- We will ask the question using a different ```session_id``` for this time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='수학에 능숙한 도우미입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 12, 'prompt_tokens': 101, 'total_tokens': 113, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_d28bcae782', 'finish_reason': 'stop', 'logprobs': None}, id='run-7625665b-73b6-43c4-aebf-28a465054aa9-0', usage_metadata={'input_tokens': 101, 'output_tokens': 12, 'total_tokens': 113, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Query with different session ID\n",
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"Please translate the previous response to Korean\"},\n",
    "    config={\"configurable\": {\"session_id\": \"redis456\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**[Note]** The last response will be inaccurate because there's no conversation history associated with that session ID ```redis456```."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-opentutorial-A2cWC-y3-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
